/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver;

import android.view.View;
import android.view.animation.Animation;

import com.hercules.android.receiver.connection.ServerConnection;
import com.hercules.android.receiver.connection.ServerConnectionException;
import com.hercules.android.receiver.handlers.commands.XREAggregateCommandHandler;
import com.hercules.android.receiver.model.events.XREConnectEvent;
import com.hercules.android.receiver.painting.XREObjectPainterUtils;
import com.hercules.android.receiver.painting.views.AndroidXREView;

import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;

/**
 * This class is the central point of the receiver application. It is created when the user selects a server to connect
 * to, and is initialized with a view that will be the container for other views.
 * <p>
 * <b>Sample usage:</b>
 *
 * <pre>
 * // AndroidReceiver can be created via default constructor
 * AndroidReceiver receiver = new AndroidReceiver();
 *
 * // after creation, all required properties must be injected
 * receiver.setLogger(Logger.getLogger(&quot;test_logger&quot;));
 *
 * // error handler will handle errors before they are thrown
 * receiver.setErrorHandler(new MockErrorHandler());
 *
 * // set server connection that will be used
 * receiver.setServerConnection(new MockServerConnection());
 *
 * // set aggregate command handler
 * receiver.setCommandHandler(new XREAggregateCommandHandler());
 *
 * // set various required data fields
 * receiver.setApplicationData(new ApplicationData());
 * receiver.setApplicationConnectData(new ApplicationConnectData());
 * receiver.getApplicationConnectData().setApplicationName(&quot;app_name&quot;);
 * receiver.getApplicationConnectData().setDeviceCaps(new XREDeviceCapabilities());
 *
 * // also we can set optional fields
 * receiver.setMaxBadCommandAcceptCount(33);
 * receiver.setOnShutdownWaitInterval(1);
 *
 * try {
 *     // now we can call connect method
 *     receiver.connect();
 *
 *     // do something useful here
 *
 *     // call shutdown in the end, onShutdownCommand will be called if corresponding command will be receiver, or
 *     // if shutdown wait interval elapse
 *     receiver.shutdown();
 * } catch (AndroidReceiverException e) {
 *     // handle exception
 * }
 * </pre>
 * <p>
 * <b>Thread Safety:</b> This class is mutable and not thread safe, but the business methods will be effectively
 * thread-safe.
 *
 * @author argolite, 7realm
 * @author 7realm
 * @version 1.0
 */
public class AndroidReceiver {
    /** Represents default value of shutdown wait interval. */
    private static final int DEFAULT_WAIT_INTERVAL = 5;
    /** Represents default value for maximum bad command count. */
    private static final int DEFAULT_BAD_COMMAND_COUNT = 10;
    /** Represents count of milliseconds per second. */
    private static final int MILLIS_PER_SECOND = 1000;

    /**
     * The logger used for logging errors and activity.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private Logger logger = Logger.getLogger(AndroidReceiver.class.getSimpleName());

    /**
     * The server connection used by the receiver application to send events and receive commands.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private ServerConnection serverConnection;

    /**
     * The root View for this receiver.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private View rootView;

    /**
     * The all the needed data for the first onConnect event.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private ApplicationConnectData applicationConnectData;

    /**
     * The command handler used to handle the command messages received from the server connection.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private XREAggregateCommandHandler commandHandler;

    /**
     * The number of bad commands that are accepted from the server. A bad command may have wrong command index, wrong
     * ids and so on. If the limit is reached, shutdown() should be called. The setter in ApplicationData for
     * incrementing the number of bad commands will call the shutdown() method. The default value is 10.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private int maxBadCommandAcceptCount = DEFAULT_BAD_COMMAND_COUNT;

    /**
     * This entity stores the runtime values needed by the receiver in order to display the application.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private ApplicationData applicationData;

    /**
     * The class used for handling errors.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private AndroidReceiverErrorHandler errorHandler;

    /**
     * The time in seconds after the shutdown event is sent for this receiver to act on its own to shut things down.
     * <p>
     * It is set in the setter. It can be retrieved in the getter. It may have any value. This field will be injected by
     * the application using this class.
     */
    private int onShutdownWaitInterval = DEFAULT_WAIT_INTERVAL;

    /**
     * The flag used to track the state of the connection.
     * <p>
     * The reference is created in on class init. The onShutdownCommand method will set it to true. The connect method
     * will set it to false. The AtomicBoolean instance will never be null. The underlying value can be any value. The
     * AtomicBoolean instance will never change. The underlying value accessed as stated in the Usage section.
     * <p>
     * <b>Thread Safety:</b> Because this field may be set and checked by multiple threads, it is wrapped in a
     * thread-safe AtomicBoolean.
     */
    private final AtomicBoolean shutdown = new AtomicBoolean(false);

    /** Represents the name of this class. */
    private final String className = getClass().getName();
	
    /** 
     * Represents the local behavior data. 
     */
    private LocalBehaviorData localBehaviorData = null;
    
    /**
     * Default empty constructor.
     */
    public AndroidReceiver() {
        // empty
    }

    /**
     * Connects to the server. It may be a first time connection, or a reconnection attempt.
     * <p>
     * AndroidAssembly1 added isReconnect parameter
     *
     * @param isReconnect indicates if this is reconnect
     * @throws IllegalStateException if used properties of this instance are not set
     * @throws IllegalArgumentException if this is reconnect and applicationData.sessionGUID is not correct
     * @throws AndroidReceiverException if there is an error during execution
     */
    public void connect(boolean isReconnect) throws AndroidReceiverException {
        String methodName = "connect";
        LogUtil.enter(logger, this, methodName);

        try {
            // check state
            ReceiverUtil.checkState(errorHandler, "errorHandler");
            ReceiverUtil.checkState(serverConnection, "serverConnection");
            ReceiverUtil.checkState(applicationConnectData, "applicationConnectData");
            ReceiverUtil.checkState(applicationConnectData.getDeviceCaps(), "applicationConnectData.deviceCaps");
            ReceiverUtil.checkState(applicationConnectData.getApplicationName(),
                "applicationConnectData.applicationName");

            // prepare connect event
            XREConnectEvent event = new XREConnectEvent();
            // AndroidAssembly1 added event name
            event.setName(XREConnectEvent.ON_CONNECT_EVENT_NAME);
            event.setDeviceCaps(applicationConnectData.getDeviceCaps());
            event.setApplicationName(applicationConnectData.getApplicationName());
            event.setMinimumVersion(applicationConnectData.getMinimumVersion());
            event.setAuthenticationToken(applicationConnectData.getAuthenticationToken());
            event.setAppParams(applicationConnectData.getAppParams());
            // AndroidAssembly1 added command index
            event.setCurrentCommandIndex(applicationConnectData.getCurrentCommandIndex());

            // AndroidAssembly1 added is reconnect parameter
            if (isReconnect) {
                if (applicationData == null || applicationData.getSessionGUID() == null) {
                    throw new AndroidReceiverException(
                        "Reconnect flag is true but application data or session GUID is not set.");
                }

                // reconnecting
                event.setReconnect(true);
                event.setSessionGUID(applicationData.getSessionGUID());
                event.setCurrentCommandIndex(applicationData.getCurrentCommandIndex());
            } else {
                // launching first connection event
                event.setReconnect(false);

                // get session guid or generate new one
                String guid = applicationData == null ? null : applicationData.getSessionGUID();
                if (guid == null) {
                //    guid = UUID.randomUUID().toString();
                }
                event.setSessionGUID(guid);
            }

            shutdown.set(false);

            // send event
            serverConnection.connect(event, this);
          
        } catch (ServerConnectionException e) {
            errorHandler.onConnectError(this, e.getMessage());
            throw LogUtil.error(logger, this, methodName, new AndroidReceiverException(
                "Error occurred while connecting to server.", e));
        } catch (IllegalStateException e) {
            throw LogUtil.error(logger, this, methodName, e);
        } catch (AndroidReceiverException e) {
            throw LogUtil.error(logger, this, methodName, e);
        }

        LogUtil.exit(logger, this, methodName);
    }

    /**
     * This method sends an onShutdown event. The actual shutdown will happen when the server sends a SHUTDOWN command.
     * However, it sets a timer that will execute onShutdownCommand method, just in case the command will not arrive.
     *
     * @throws IllegalStateException if used properties of this instance are not set
     * @throws IllegalArgumentException if applicationData.sessionGUID is not correct
     * @throws AndroidReceiverException if there is an error during execution
     */
    public void shutdown(boolean preserveSession) throws AndroidReceiverException {
        String methodName = "shutdown";
        LogUtil.enter(logger, this, methodName);

        try {
            // check state
            ReceiverUtil.checkState(errorHandler, "errorHandler");
            ReceiverUtil.checkState(applicationData, "applicationData");
            ReceiverUtil.checkState(serverConnection, "serverConnection");

            // create shutdown event
            // XREEvent event = new XREShutdownEvent();
            // event.setName(XREShutdownEvent.ON_SHUTDOWN_EVENT_NAME);

            // send event
            // serverConnection.send(event, this);
            
            serverConnection.close(applicationData.getSessionGUID());
            
            // launch a new thread to call wait a moment to call shutdown
            // new OnShutdownHandler().start();
            doShutdown(preserveSession);
   

        } catch (ServerConnectionException e) {
            errorHandler.onShutdownError(this, e.getMessage());
            throw LogUtil.error(logger, this, methodName, new AndroidReceiverException(
                "Error occurred while sending shutdown event.", e));
        } catch (IllegalStateException e) {
            throw LogUtil.error(logger, this, methodName, e);
        }

        LogUtil.exit(logger, this, methodName);
    }

    /**
     * Closes the connection. Eliminates the session if asked.
     * <p>
     * AndroidAssembly3 closes the receiver
     *
     * @param preserveSession flag whether the current session should be preserved
     * @throws IllegalStateException if used properties of this instance are not set
     * @throws IllegalArgumentException if applicationData.sessionGUID is not correct
     * @throws AndroidReceiverException if there is an error during execution
     */
    @SuppressWarnings("boxing")
    public void doShutdown(boolean preserveSession) throws AndroidReceiverException {
        String methodName = "doShutdown";
        LogUtil.enter(logger, this, methodName, preserveSession);

        try {
            if (shutdown.get()) {
                return;
            }

            // check state
            ReceiverUtil.checkState(errorHandler, "errorHandler");
            ReceiverUtil.checkState(applicationData, "applicationData");
            ReceiverUtil.checkState(serverConnection, "serverConnection");

            // reset internal state
            if (!preserveSession) {
                // AndroidAssembly1 added clearing of root view
                // remove all views
                final AndroidXREView androidXREView = (AndroidXREView) rootView;
            	clearAnimation(androidXREView);
                androidXREView.removeAllViews();
                shutdown.set(true);

                while (!shutdown.get()) {
                	try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
                }
                
                XREObjectPainterUtils.finalize(this);
                applicationData = new ApplicationData();
            } else {
            	shutdown.set(true);
            }

            Map<Integer, AndroidReceiver> nestedReceivers = applicationData.getNestedXREApplicationViewsIdToReceiver();
            if (nestedReceivers != null) {
                for (AndroidReceiver nestedReceiver : nestedReceivers.values()) {
                    if (nestedReceiver != null) {
                        nestedReceiver.doShutdown(preserveSession);
                    }
                }
            }
            
            // log exit
            //logger.exiting(className, methodName);
        } catch (IllegalStateException e) {
            throw LogUtil.error(logger, this, methodName, e);
        }

        LogUtil.exit(logger, this, methodName);
    }

    private void clearAnimation(final AndroidXREView androidXREView) {
    	for (int i = 0; i < androidXREView.getChildCount(); ++i) {
    		View child = androidXREView.getChildAt(i);
    		if (child instanceof AndroidXREView) {
    			Animation a = child.getAnimation();
    			if (a != null) {
	    			a.setAnimationListener(null);
	    			a.cancel();
    			}
    		}
    	}
    }
    
    /**
     * Gets the logger used for logging errors and activity.
     *
     * @return the logger used for logging errors and activity
     */
    public Logger getLogger() {
        return logger;
    }

    /**
     * Sets the logger used for logging errors and activity.
     *
     * @param logger the logger used for logging errors and activity
     */
    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    /**
     * Gets the server connection used by the receiver application to send events and receive commands.
     *
     * @return the server connection used by the receiver application to send events and receive commands
     */
    public ServerConnection getServerConnection() {
        return serverConnection;
    }

    /**
     * Sets the server connection used by the receiver application to send events and receive commands.
     *
     * @param serverConnection the server connection used by the receiver application to send events and receive
     * commands
     */
    public void setServerConnection(ServerConnection serverConnection) {
        this.serverConnection = serverConnection;
    }

    /**
     * Gets the root View for this receiver.
     *
     * @return the root View for this receiver
     */
    public View getRootView() {
        return rootView;
    }

    /**
     * Sets the root View for this receiver.
     *
     * @param rootView the root View for this receiver
     */
    public void setRootView(View rootView) {
        this.rootView = rootView;
    }

    /**
     * Gets the all the needed data for the first onConnect event.
     *
     * @return the all the needed data for the first onConnect event
     */
    public ApplicationConnectData getApplicationConnectData() {
        return applicationConnectData;
    }

    /**
     * Sets the all the needed data for the first onConnect event.
     *
     * @param applicationConnectData the all the needed data for the first onConnect event
     */
    public void setApplicationConnectData(ApplicationConnectData applicationConnectData) {
        this.applicationConnectData = applicationConnectData;
    }

    /**
     * Gets the command handler used to handle the command messages received from the server connection.
     *
     * @return the command handler used to handle the command messages received from the server connection
     */
    public XREAggregateCommandHandler getCommandHandler() {
        return commandHandler;
    }

    /**
     * Sets the command handler used to handle the command messages received from the server connection.
     *
     * @param commandHandler the command handler used to handle the command messages received from the server connection
     */
    public void setCommandHandler(XREAggregateCommandHandler commandHandler) {
        this.commandHandler = commandHandler;
    }

    /**
     * Gets the number of bad commands that are accepted from the server.
     * <p>
     * A bad command may have wrong command index, wrong ids and so on. If the limit is reached, shutdown() should be
     * called. The setter in ApplicationData for incrementing the number of bad commands will call the shutdown()
     * method. The default value is 10.
     *
     * @return the number of bad commands that are accepted from the server
     */
    public int getMaxBadCommandAcceptCount() {
        return maxBadCommandAcceptCount;
    }

    /**
     * Sets the number of bad commands that are accepted from the server.
     * <p>
     * A bad command may have wrong command index, wrong ids and so on. If the limit is reached, shutdown() should be
     * called. The setter in ApplicationData for incrementing the number of bad commands will call the shutdown()
     * method. The default value is 10.
     *
     * @param maxBadCommandAcceptCount the number of bad commands that are accepted from the server
     */
    public void setMaxBadCommandAcceptCount(int maxBadCommandAcceptCount) {
        this.maxBadCommandAcceptCount = maxBadCommandAcceptCount;
    }

    /**
     * Gets this entity stores the runtime values needed by the receiver in order to display the application.
     *
     * @return this entity stores the runtime values needed by the receiver in order to display the application
     */
    public ApplicationData getApplicationData() {
        return applicationData;
    }

    /**
     * Sets this entity stores the runtime values needed by the receiver in order to display the application.
     *
     * @param applicationData this entity stores the runtime values needed by the receiver in order to display the
     * application
     */
    public void setApplicationData(ApplicationData applicationData) {
        this.applicationData = applicationData;
    }

    /**
     * Gets the class used for handling errors.
     *
     * @return the class used for handling errors
     */
    public AndroidReceiverErrorHandler getErrorHandler() {
        return errorHandler;
    }

    /**
     * Sets the class used for handling errors.
     *
     * @param errorHandler the class used for handling errors
     */
    public void setErrorHandler(AndroidReceiverErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
    }

    /**
     * Gets the time in seconds after the shutdown event is sent for this receiver to act on its own to shut things
     * down.
     *
     * @return the time in seconds after the shutdown event is sent for this receiver to act on its own to shut things
     * down
     */
    public int getOnShutdownWaitInterval() {
        return onShutdownWaitInterval;
    }

    /**
     * Sets the time in seconds after the shutdown event is sent for this receiver to act on its own to shut things
     * down.
     *
     * @param onShutdownWaitInterval the time in seconds after the shutdown event is sent for this receiver to act on
     * its own to shut things down
     */
    public void setOnShutdownWaitInterval(int onShutdownWaitInterval) {
        this.onShutdownWaitInterval = onShutdownWaitInterval;
    }
    
    /**
	 * @param localBehaviorData the localBehaviorData to set
	 */
	public void setLocalBehaviorData(LocalBehaviorData localBehaviorData) {
		this.localBehaviorData = localBehaviorData;
	}

	/**
	 * @return the localBehaviorData
	 */
	public LocalBehaviorData getLocalBehaviorData() {
		return localBehaviorData;
	}

}